CDKでAWS Lambdaのパッケージフォーマットにコンテナイメージを指定してデプロイしてみた
はじめに
先日のre:Invent 2020にて、Lambdaのパッケージフォーマットとして、従来のZIP形式に加えてコンテナイメージがサポートされました。詳しくは下記を参照してください。
【速報】Lambdaのパッケージフォーマットとしてコンテナイメージがサポートされるようになりました!! #reinvent
CDKでLambdaのパッケージフォーマットにコンテナイメージの定義ができると実践投入や構成を考えやすなーと思っていたところ、CDKのv1.76.0で定義できるようになっていたため、今回はそちらを試します。
以下注意点です。ご了承お願いします。
- 本記事は、CDK v1.76.0ベースで記載した記事となります。CDKのバージョンが上がった場合には、記事通りやっても動かない可能性があります
- 作成する関数は、Node12系です。別言語で作成したい方は、一部しか参考になりません
構成
概要
今回作る構成は、パッケージフォーマットにコンテナイメージを指定したLambda関数2つ(以降aLambda,bLambdaとします)です
両関数とも設定するコンテナイメージは同じですが、エントリポイントをCMDで切り替えるため処理的には独立した関数となります
記事内で全てソースコードを貼るのは分量が多いので、完成版のリポジトリを作りました。適宜参照して頂けたらと思います
shuntaka9576/lambda-container-image-sample
ディレクトリ構成
ざっくりディレクト構成とファイルの説明を下記に示します
├── bin │ └── cdk.ts // スタックデプロイの起点となるtsファイル ├── lib │ ├── cdk-ecr-stack.ts // ECRリポジトリのスタック定義 │ └── cdk-lambda-stack.ts // aLambda,bLambdaのスタック定義 ├── src │ └── lambda │ └── handler │ └── apig-trigger │ ├── apig-a-handler.ts // aLambdaのエントリポイント │ └── apig-b-handler.ts // bLambdaのエントリポイント ├── dist │ ├── a │ │ └── index.js // aLambdaをwebpackでバンドルした資材が配備される(gitignoreされています) │ └── b │ └── index.js // bLambdaをwebpackでバンドルした資材が配備される(上記同様) ├── Dockerfile // Lambdaのパッケージフォーマットとして定義したDockerfile ├── Makefile ├── cdk.json ├── package.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock
構築
前述の構成を構築していきます
ソースのクローン
git clone https://github.com/shuntaka9576/lambda-container-image-sample.git
ECRの作成
今回作成するCloudFormationのスタックは以下の2つです
- (A) ECRリポジトリのスタック
- (B) ECRリポジトリのコンテナイメージを参照した2つのLambdaが定義されたスタック
分けた理由としては、(A)と(B)ではデプロイサイクルが異なるためです。(B)ではcdk.json
を利用して(A)を参照します。
ECRリポジトリのCDK定義は下記です。
export class EcrStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const repository = new ecr.Repository(this, "SampleNodeApp", { repositoryName: "sample-node-app", imageScanOnPush: true, }); // コンテナイメージのライフサイクル設定 // (tagのprefixがprodの場合は9999まで保持し、prefixがない場合30日経過後削除する設定) repository.addLifecycleRule({ tagPrefixList: ["prod"], maxImageCount: 9999, }); repository.addLifecycleRule({ maxImageAge: cdk.Duration.days(30) }); // 作成されたECRリポジトリのARNをコマンド実行時に出力する設定 new cdk.CfnOutput(this, "ecrArn", { value: `${repository.repositoryArn}`, }); } }
assume-roleして、cdkのデプロイします
yarn cdk deploy -c stageName=dev ecr
デプロイすると、ARNが出力されますので、メモしてください
Outputs: ecr.ecrArn = arn:aws:ecr:ap-northeast-1:[数値12桁くらい]:repository/sample-node-app
AWSコンソールでsample-node-app
のリポジトリが出来ていることを確認しましょう
Lambdaにデプロイするコンテナイメージの作成
コンテナイメージのディレクトリ構成は、下記の通りです。
/ (ルート) ├── functions ├── a │ └── index.js // aLambdaをwebpackでバンドルした資材 └── b └── index.js // bLambdaをwebpackでバンドルした資材
コンテナイメージのFrom
には、AWSが提供しているLambdaのAWSベースイメージを指定します。
Dockerfileは下記になります
FROM public.ecr.aws/lambda/nodejs:12 ARG FUNCTION_DIR="/functions" COPY dist/ $FUNCTION_DIR
(Lambdaで動かせるコンテナイメージには要件があります。詳しくは公式ドキュメントを参照してください)
コンテナをビルドします。AWSコンソールのECRリポジトリURIを取得して指定します。
yarn build # webpackでtsファイルをビルド、バンドル docker image build . -t [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest # コンテナイメージの作成とlatestタグの付与 docker tag [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:d504fc5 # 過去のイメージを追えるようにリビジョンタグの付与
正しくコピーされていることを確認するためコンテナにexecします
$ docker run -it --entrypoint '' [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest /bin/sh sh-4.2# cd /functions/a/ sh-4.2# ls index.js sh-4.2# cd /functions/b/ sh-4.2# ls index.js
コンテナイメージの/functions
配下にwebpackでバンドルしたファイル(ローカルの/dist
のディクトリ・ファイル)が入っていることが確認できました。
ローカルでコンテナイメージの動作確認をする
ランタイムインターフェイスエミュレーターを使用して、アプリケーションをローカルでテストすることができます。
/functions/a/index.js
と /functions/b/index.js
に関数がありますので、それぞれCMDに指定して動作確認します。
コンテナが起動。待機状態となる
$ docker run -p 9000:8080 [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest /functions/a/index.handler time="2020-12-04T16:16:13.08" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"
別のshellを起動して、curlを実行
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"eventName": "test"}'
下記のようにいつもCloudWatchで見るログが、ローカルのシェル上から確認できます。感動ですね。
$ docker run -p 9000:8080 [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest /functions/a/index.handler time="2020-12-04T16:16:13.08" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)" time="2020-12-04T16:17:08.183" level=info msg="extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory" time="2020-12-04T16:17:08.183" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory" START RequestId: 45bc127a-ca3e-4f3f-985f-2dd3f5c5f413 Version: $LATEST 2020-12-04T16:17:08.275Z 45bc127a-ca3e-4f3f-985f-2dd3f5c5f413 INFO event: {"eventName":"test"} 2020-12-04T16:17:08.279Z 45bc127a-ca3e-4f3f-985f-2dd3f5c5f413 INFO start a handler END RequestId: 45bc127a-ca3e-4f3f-985f-2dd3f5c5f413 REPORT RequestId: 45bc127a-ca3e-4f3f-985f-2dd3f5c5f413 Init Duration: 0.29 ms Duration: 98.05 ms Billed Duration: 100 ms Memory Size: 3008 MB Max Memory Used: 3008 MB
下記のコマンドを実行すれば、functions/b/index.handler
も実行可能です。
$ docker run -p 9000:8080 [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest /functions/b/index.handler
作成したコンテナイメージをECRへpushする
まずデプロイ環境へassume-roleを済ませた後、ECRにログインする必要があります。下記の画像を参考にコピーしてください。
ECRへイメージをpushします
aws ecr get-login-pasword ... # 上記でコピーしたECRのログインコマンドを実行 docker push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:latest docker push [AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/sample-node-app:d504fc5
pushされていることと、タグにlatest
とd504fc5
が付与されていることを確認します
Lambdaスタックをデプロイする
ECRの作成の項で出力されたARNをcdk.jsonのrepositoryArnのvalueに記述してください
{ "app": "npx ts-node bin/cdk.ts", "context": { "dev" : { // arn:.. の部分を変更する "repositoryArn": "arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/sample-node-app" }, "prd" : { } } }
Lambdaが定義されたスタックは下記の通りです
import * as cdk from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; import * as ecr from "@aws-cdk/aws-ecr"; import { Role, ServicePrincipal, ManagedPolicy } from "@aws-cdk/aws-iam"; export class LambdaStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const stageName: string = this.node.tryGetContext("stageName"); const env: { repositoryArn: string } = this.node.tryGetContext(stageName); // ECRリポジトリをARN経由で参照 const sampleNodeAppRepository = ecr.Repository.fromRepositoryArn( this, id, env.repositoryArn ); const execLambdaRole = new Role(this, "execRole", { roleName: `${stageName}lambdaExecRole`, assumedBy: new ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName( "service-role/AWSLambdaBasicExecutionRole" ), ], }); new lambda.Function(this, "aLambda", { code: lambda.Code.fromEcrImage(sampleNodeAppRepository, { cmd: ["/functions/a/index.handler"], // リクエストのエントリーポイント tag: "latest", // コンテナイメージのタグ entrypoint: ["/lambda-entrypoint.sh"], }), role: execLambdaRole, functionName: `${stageName}-a-lambda`, runtime: lambda.Runtime.FROM_IMAGE, handler: lambda.Handler.FROM_IMAGE, timeout: cdk.Duration.seconds(10), }); new lambda.Function(this, "bLambda", { code: lambda.Code.fromEcrImage(sampleNodeAppRepository, { cmd: ["/functions/b/index.handler"], tag: "latest", entrypoint: ["/lambda-entrypoint.sh"], }), role: execLambdaRole, functionName: `${stageName}-b-lambda`, runtime: lambda.Runtime.FROM_IMAGE, handler: lambda.Handler.FROM_IMAGE, timeout: cdk.Duration.seconds(10), }); } }
Lambdaが定義されたスタックをデプロイします
yarn deploy -c stageName=dev dev-lambdas
AWSコンソールでLambdaが2つ作成されていることを確認できればOKです。
動かしてみる
dev-a-lambda
元ソースapig-a-handler.ts
の内容が実行されていることを確認できました
dev-b-lambda
元ソースapig-b-handler.ts
の内容が実行されていることを確認できました
最後に
Lambdaのパッケージフォーマットにコンテナイメージがサポートされることで、今までのLambdaアプリケーションのデプロイやロールバックの仕方がより効率的になる場合があると感じました。
またローカルでコンテナを起動することで、実際に環境へデプロイしない分、効率よく「テストを書く->失敗したらテストを修正する」のサイクルが出来そうだとも感じました。(ある程度工夫は必要ですが...)